// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.client.diff;

import static java.lang.Double.POSITIVE_INFINITY;

import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;

import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;

import java.util.Collections;
import java.util.List;

public class SideBySide extends DiffScreen {
  interface Binder extends UiBinder<FlowPanel, SideBySide> {}
  private static final Binder uiBinder = GWT.create(Binder.class);

  @UiField(provided = true)
  SideBySideTable diffTable;

  private CodeMirror cmA;
  private CodeMirror cmB;

  private ScrollSynchronizer scrollSynchronizer;

  private SideBySideChunkManager chunkManager;
  private SideBySideCommentManager commentManager;
  private SideBySideSkipManager skipManager;

  public SideBySide(
      PatchSet.Id base,
      PatchSet.Id revision,
      String path,
      DisplaySide startSide,
      int startLine) {
    super(base, revision, path, startSide, startLine, DiffView.SIDE_BY_SIDE);

    diffTable = new SideBySideTable(this, base, revision, path);
    add(uiBinder.createAndBindUi(this));
    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
  }

  @Override
  ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
      final CommentsCollections comments) {
    return new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
      @Override
      protected void preDisplay(ConfigInfoCache.Entry result) {
        commentManager = new SideBySideCommentManager(
            SideBySide.this,
            base, revision, path,
            result.getCommentLinkProcessor(),
            getChangeStatus().isOpen());
        setTheme(result.getTheme());
        display(comments);
        header.setupPrevNextFiles(comments);
      }
    };
  }

  @Override
  public void onShowView() {
    super.onShowView();

    operation(new Runnable() {
      @Override
      public void run() {
        resizeCodeMirror();
        chunkManager.adjustPadding();
        cmA.refresh();
        cmB.refresh();
      }
    });
    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
    diffTable.refresh();

    if (getStartLine() == 0) {
      DiffChunkInfo d = chunkManager.getFirst();
      if (d != null) {
        if (d.isEdit() && d.getSide() == DisplaySide.A) {
          setStartSide(DisplaySide.B);
          setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1);
        } else {
          setStartSide(d.getSide());
          setStartLine(d.getStart() + 1);
        }
      }
    }
    if (getStartSide() != null && getStartLine() > 0) {
      CodeMirror cm = getCmFromSide(getStartSide());
      cm.scrollToLine(getStartLine() - 1);
      cm.focus();
    } else {
      cmA.setCursor(Pos.create(0));
      cmA.focus();
    }
    if (Gerrit.isSignedIn() && prefs.autoReview()) {
      header.autoReview();
    }
    prefetchNextFile();
  }

  @Override
  void registerCmEvents(final CodeMirror cm) {
    super.registerCmEvents(cm);

    KeyMap keyMap = KeyMap.create()
        .on("Shift-A", diffTable.toggleA())
        .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
        .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B));
    cm.addKeyMap(keyMap);
    maybeRegisterRenderEntireFileKeyMap(cm);
  }

  @Override
  public void registerKeys() {
    super.registerKeys();

    getKeysNavigation().add(
        new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
        new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
    getKeysAction().add(new KeyCommand(
        KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
      @Override
      public void onKeyPress(KeyPressEvent event) {
        diffTable.toggleA().run();
      }
    });

    registerHandlers();
  }

  @Override
  FocusHandler getFocusHandler() {
    return new FocusHandler() {
      @Override
      public void onFocus(FocusEvent event) {
        cmB.focus();
      }
    };
  }

  private void display(final CommentsCollections comments) {
    final DiffInfo diff = getDiff();
    setThemeStyles(prefs.theme().isDark());
    setShowIntraline(prefs.intralineDifference());
    if (prefs.showLineNumbers()) {
      diffTable.addStyleName(SideBySideTable.style.showLineNumbers());
    }

    cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
    cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);

    cmA.extras().side(DisplaySide.A);
    cmB.extras().side(DisplaySide.B);
    setShowTabs(prefs.showTabs());

    chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
    skipManager = new SideBySideSkipManager(this, commentManager);

    operation(new Runnable() {
      @Override
      public void run() {
        // Estimate initial CodeMirror height, fixed up in onShowView.
        int height = Window.getClientHeight()
            - (Gerrit.getHeaderFooterHeight() + 18);
        cmA.setHeight(height);
        cmB.setHeight(height);

        render(diff);
        commentManager.render(comments, prefs.expandAllComments());
        skipManager.render(prefs.context(), diff);
      }
    });

    registerCmEvents(cmA);
    registerCmEvents(cmB);
    scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB,
            chunkManager.getLineMapper());

    setPrefsAction(new PreferencesAction(this, prefs));
    header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks());
    scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());

    setupSyntaxHighlighting();
  }

  private List<InlineHyperlink> getUnifiedDiffLink() {
    InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
    toUnifiedDiffLink.setHTML(
        new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
    toUnifiedDiffLink.setTargetHistoryToken(
        Dispatcher.toUnified(base, revision, path));
    toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
    return Collections.singletonList(toUnifiedDiffLink);
  }

  @Override
  CodeMirror newCm(
      DiffInfo.FileMeta meta,
      String contents,
      Element parent) {
    return CodeMirror.create(parent, Configuration.create()
      .set("cursorBlinkRate", prefs.cursorBlinkRate())
      .set("cursorHeight", 0.85)
      .set("inputStyle", "textarea")
      .set("keyMap", "vim_ro")
      .set("lineNumbers", prefs.showLineNumbers())
      .set("lineWrapping", false)
      .set("matchBrackets", prefs.matchBrackets())
      .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
      .set("readOnly", true)
      .set("scrollbarStyle", "overlay")
      .set("showTrailingSpace", prefs.showWhitespaceErrors())
      .set("styleSelectedText", true)
      .set("tabSize", prefs.tabSize())
      .set("theme", prefs.theme().name().toLowerCase())
      .set("value", meta != null ? contents : "")
      .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
  }

  @Override
  void setShowLineNumbers(boolean b) {
    cmA.setOption("lineNumbers", b);
    cmB.setOption("lineNumbers", b);
    if (b) {
      diffTable.addStyleName(SideBySideTable.style.showLineNumbers());
    } else {
      diffTable.removeStyleName(SideBySideTable.style.showLineNumbers());
    }
  }

  @Override
  void setSyntaxHighlighting(boolean b) {
    final DiffInfo diff = getDiff();
    if (b) {
      injectMode(diff, new AsyncCallback<Void>() {
        @Override
        public void onSuccess(Void result) {
          if (prefs.syntaxHighlighting()) {
            cmA.setOption("mode", getContentType(diff.metaA()));
            cmB.setOption("mode", getContentType(diff.metaB()));
          }
        }

        @Override
        public void onFailure(Throwable caught) {
          prefs.syntaxHighlighting(false);
        }
      });
    } else {
      cmA.setOption("mode", (String) null);
      cmB.setOption("mode", (String) null);
    }
  }

  @Override
  void setAutoHideDiffHeader(boolean hide) {
    scrollSynchronizer.setAutoHideDiffTableHeader(hide);
  }

  CodeMirror otherCm(CodeMirror me) {
    return me == cmA ? cmB : cmA;
  }

  @Override
  CodeMirror getCmFromSide(DisplaySide side) {
    return side == DisplaySide.A ? cmA : cmB;
  }

  @Override
  int getCmLine(int line, DisplaySide side) {
    return line;
  }

  @Override
  Runnable updateActiveLine(final CodeMirror cm) {
    final CodeMirror other = otherCm(cm);
    return new Runnable() {
      @Override
      public void run() {
        // The rendering of active lines has to be deferred. Reflow
        // caused by adding and removing styles chokes Firefox when arrow
        // key (or j/k) is held down. Performance on Chrome is fine
        // without the deferral.
        //
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
          @Override
          public void execute() {
            operation(new Runnable() {
              @Override
              public void run() {
                LineHandle handle =
                    cm.getLineHandleVisualStart(cm.getCursor("end").line());
                if (!cm.extras().activeLine(handle)) {
                  return;
                }

                LineOnOtherInfo info =
                    lineOnOther(cm.side(), cm.getLineNumber(handle));
                if (info.isAligned()) {
                  other.extras().activeLine(other.getLineHandle(info.getLine()));
                } else {
                  other.extras().clearActiveLine();
                }
              }
            });
          }
        });
      }
    };
  }

  private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) {
    final CodeMirror cmDst = getCmFromSide(sideDst);
    if (cmDst == cmSrc) {
      return new Runnable() {
        @Override
        public void run() {
        }
      };
    }

    final DisplaySide sideSrc = cmSrc.side();
    return new Runnable() {
      @Override
      public void run() {
        if (cmSrc.extras().hasActiveLine()) {
          cmDst.setCursor(Pos.create(lineOnOther(
              sideSrc,
              cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine()));
        }
        cmDst.focus();
      }
    };
  }

  void syncScroll(DisplaySide masterSide) {
    if (scrollSynchronizer != null) {
      scrollSynchronizer.syncScroll(masterSide);
    }
  }

  @Override
  void operation(final Runnable apply) {
    cmA.operation(new Runnable() {
      @Override
      public void run() {
        cmB.operation(new Runnable() {
          @Override
          public void run() {
            apply.run();
          }
        });
      }
    });
  }

  @Override
  CodeMirror[] getCms() {
    return new CodeMirror[]{cmA, cmB};
  }

  @Override
  SideBySideTable getDiffTable() {
    return diffTable;
  }

  @Override
  SideBySideChunkManager getChunkManager() {
    return chunkManager;
  }

  @Override
  SideBySideCommentManager getCommentManager() {
    return commentManager;
  }

  @Override
  SideBySideSkipManager getSkipManager() {
    return skipManager;
  }
}
